package ss.pku.jsa.type.inference;

import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.Stack;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ArrayAccess;
import org.eclipse.jdt.core.dom.ArrayCreation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.BreakStatement;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConditionalExpression;
import org.eclipse.jdt.core.dom.ConstructorInvocation;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.LabeledStatement;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NullLiteral;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PostfixExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperFieldAccess;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

import ss.pku.jsa.type.constraint.solver.QError;


@SuppressWarnings({ "unchecked", "rawtypes" })
public class ConstraintVisitor extends ASTVisitor {
	protected CompilationUnit compilationUnit = null;
	protected String fileName = null;
	boolean original;
	private Stack<MethodQType> currentMethodStack = new Stack();

	private Stack<ObjectQType> fieldContext = new Stack();

	Analysis analysis = null;
	static final String QTYPE_PROPERTY = "QType";
	static final String OBJECT_CONTEXT_PROPERTY = "object_context";

	// private static boolean constructorAnnouncementMade = false;

	public ConstraintVisitor(CompilationUnit compilationUnit, String fileName,
			Analysis analysis, boolean original) {
		this.compilationUnit = compilationUnit;
		this.fileName = fileName;
		this.analysis = analysis;
		this.original = original;
	}

	public void endVisit(ArrayAccess node) {
		Location l = getLocation(node);
		ArrayQType arrayQType;
		try {
			arrayQType = (ArrayQType) deref(getNodeQType(node.getArray()));
		} catch (ClassCastException localClassCastException) {
			throw new IllegalArgumentException(
					"Array access on non-ArrayQType: " + node + " <"
							+ getNodeQType(node.getArray()).getKind() + "> at "
							+ l + " qt: " + getNodeQType(node.getArray()));
		}

		if (arrayQType == null) {
			throw new IllegalArgumentException("Array access on null QType: "
					+ node + " at " + l);
		}
		RefQType nodeQt = null;

		nodeQt = arrayQType.getElement();

		if (nodeQt == null) {
			throw new RuntimeException("element qtype is null for " + node
					+ " at " + l + "\nnode:" + node.getArray());
		}

		setNodeQType(node, nodeQt);
		setNodeObjectContext(node, arrayQType);
	}

	public void endVisit(ArrayCreation node) {
		Location l = getLocation(node);

		if (node.getInitializer() != null) {
			ArrayInitializer init = node.getInitializer();
			ITypeBinding t = init.resolveTypeBinding();
			int d = t.getDimensions();
			ArrayQType qt = (ArrayQType) QType.getFreshQType(
					t.getElementType(), d, l, "arrayInitializer", false, true);
			setNodeQType(node, qt);

			if (qt.getElement() != null)
				for (Iterator it = node.getInitializer().expressions()
						.iterator(); it.hasNext();) {
					QType initQType = getNodeQType((Expression) it.next());
					Constraint.addConstraint(deref(initQType), qt.getElement()
							.deref(), QualGate.EMPTY, l, 0, QError.INIT,
							Constraint.Kind.LT);
				}
		}
	}

	public boolean visit(Assignment node) {
		if (node.getOperator().equals(Assignment.Operator.PLUS_ASSIGN)) {
			ITypeBinding t = node.resolveTypeBinding();
			if ((t != null) && (!t.isPrimitive())) {
				Expression right = node.getRightHandSide();
				MethodInvocation m = wrapper(right);
				node.setRightHandSide(m);
				m.arguments().add(right);
			}
		}

		return true;
	}

	public void endVisit(Assignment node) {
		Expression left = node.getLeftHandSide();
		Expression right = node.getRightHandSide();

		Location l = getLocation(node);
		RefQType leftQt;
		try {
			leftQt = (RefQType) getNodeQType(left);
		} catch (ClassCastException localClassCastException) {
			throw new IllegalArgumentException(
					"Non-ref type on LHS of assignment " + node + " at " + l);
		}

		if (leftQt == null) {
			throw new IllegalArgumentException(
					"null QType on LHS of assignment " + node + " at " + l);
		}
		javariAssignRule(left, l, QError.JAVARI);

		QType rightQt = getNodeQType(right);

		Constraint.addConstraint(deref(rightQt), deref(leftQt), QualGate.EMPTY,
				l, 0, QError.ASSIGN, Constraint.Kind.LT);

		setNodeQType(node, rightQt);
	}

	private boolean inConstructor() {
		MethodQType m = getCurrentMethod();
		if (m == null) {
			return false;
		}

		return m.isConstructorQType();
	}

	private void javariAssignRule(Expression target, Location l, QError error) {
		boolean showdebug = false;

		if (!JqualPlugin.inferReadonly) {
			if (showdebug)
				JqualPlugin.out.println("javari: ignore " + target
						+ ", not inferReadonly");
			return;
		}

		RefQType qt = (RefQType) getNodeQType(target);

		if (qt == null) {
			if (showdebug)
				JqualPlugin.out.println("javari: ignore " + target
						+ ", as qtype is null");
			return;
		}

		makeMutable(qt, l, error);

		if (inConstructor()) {
			if (showdebug)
				JqualPlugin.out.println("javari: ignore parent of " + target
						+ ", in constructor at " + l);
			return;
		}

		if (qt.isStatic) {
			if (showdebug)
				JqualPlugin.out.println("javari: ignore parent of " + target
						+ ", as it's static");
			return;
		}

		if ((qt.getKey() != null) && (this.analysis.mutable(qt.getKey()))) {
			if (showdebug)
				JqualPlugin.out.println("javari: ignore parent of " + target
						+ ", as it is Mutable");
			return;
		}

		ObjectQType cqt = getNodeObjectContext(target);

		if (cqt == null) {
			if (showdebug)
				JqualPlugin.out.println("javari: ignore parent of " + target
						+ " at " + l + " as cqt is null");
			return;
		}

		makeMutable(cqt, l, QError.JAVARI_MUTABLE);
	}

	static void makeMutable(QType qt, Location l, QError error) {
		try {
			qt.getQual().makeMutable(l, error);
		} catch (NullPointerException localNullPointerException) {
			throw new NullPointerException("Making " + qt + " mutable at " + l);
		}
	}

	private void assignEnumType(QType qt, Location l, int val) {
		if (!JqualPlugin.inferOpaque)
			return;
		if (qt == null) {
			return;
		}
		if (!JqualPlugin.ffiInferEnum) {
			makeTransparent(qt, l, QError.FFI_CONST_TRANSP);
			return;
		}

		if ((val < 0) || (val > this.analysis.maxEnum)) {
			CQual.makeEdge(Constraint.Kind.LT, this.analysis.transparent,
					qt.getQual(), QualGate.EMPTY, l, 0,
					QError.FFI_NUM_TOO_LARGE);
		} else {
			Qualifier e = Qualifier.findQualifierConstant("enum_" + val);
			CQual.makeEdge(Constraint.Kind.LT, e, qt.getQual(), QualGate.EMPTY,
					l, 0, QError.FFI_NUM_AS_ENUM);
		}
	}

	private void makeTransparent(QType qt, Location l, QError error) {
		if ((!JqualPlugin.inferOpaque) || (!JqualPlugin.ffiInferTrans))
			return;
		if (qt == null)
			return;
		try {
			CQual.makeEdge(Constraint.Kind.LT, this.analysis.transparent,
					qt.getQual(), QualGate.EMPTY, l, 0, error);
		} catch (NullPointerException localNullPointerException) {
			throw new NullPointerException("Making " + qt + " transparent at "
					+ l);
		}
	}

	public boolean visit(NumberLiteral node) {
		if (!JqualPlugin.inferOpaque)
			return true;
		Location l = getLocation(node);
		PrimitiveQType nodeQt = (PrimitiveQType) QType.getFreshQType(
				node.resolveTypeBinding(), 0, l, "NumberLiteral",
				this.original, true);

		Object val = node.resolveConstantExpressionValue();

		if (((val instanceof Integer)) && (JqualPlugin.ffiInferEnum))
			assignEnumType(nodeQt, l, ((Integer) val).intValue());
		else {
			makeTransparent(nodeQt, l, QError.FFI_NUM_TRANSP);
		}
		setNodeQType(node, nodeQt);
		return true;
	}

	public boolean visit(StringLiteral node) {
		if (!JqualPlugin.inferOpaque)
			return true;
		Location l = getLocation(node);
		ObjectQType nodeQt = (ObjectQType) QType.getFreshQType(
				node.resolveTypeBinding(), 0, l, "StringLiteral@" + l.locIndex,
				this.original, true);
		makeTransparent(nodeQt, l, QError.FFI_CONST_TRANSP);
		setNodeQType(node, nodeQt);
		return true;
	}

	public void endVisit(CastExpression node) {
		Location l = getLocation(node);

		ITypeBinding t = node.getType().resolveBinding();
		int d = t.getDimensions();
		QType qt = QType.getFreshQType(t, d, l, "cast@" + l.getLine(), false,
				true);

		if (this.analysis.getQualifier(l) != null) {
			CQual.makeEdge(Constraint.Kind.EQ, this.analysis.getQualifier(l),
					qt.getQual(), QualGate.EMPTY, l, 0, QError.CAST);
		} else {
			QType orig = getNodeQType(node.getExpression());
			if (orig != null) {
				Constraint.addConstraint(getNodeQType(node.getExpression())
						.deref(), qt, QualGate.EMPTY, l, 0, QError.CAST,
						Constraint.Kind.LT);
				Constraint.addConstraint(qt.deref(),
						getNodeQType(node.getExpression()).deref(),
						QualGate.EMPTY, l, 0, QError.CAST, Constraint.Kind.LT);
			}
		}
		setNodeQType(node, qt);
	}

	public void endVisit(ClassInstanceCreation node) {
		Location l = getLocation(node);
		node.resolveTypeBinding();

		IMethodBinding m = node.resolveConstructorBinding();
		if (m == null) {
			this.analysis.logMissingClass(node.toString(), l);
			return;
		}
		MethodKey mkey = MethodKey.getMethodKey(m);

		MethodQType mqt = (MethodQType) this.analysis.lookupQType(mkey);
		if (mqt == null) {
			mqt = QType.getFreshQType(m, l, false);
			this.analysis.addQType(mkey, mqt);
		}

		List formalArgs = mqt.getParameterQTypes();
		Iterator args = node.arguments().iterator();
		for (Iterator formal = formalArgs.iterator(); formal.hasNext();) {
			QType argQT = getNodeQType((Expression) args.next());
			QType fqt = (QType) formal.next();

			if (JqualPlugin.contextSensitive)
				Constraint.addConstraint(deref(argQT), deref(fqt),
						QualGate.EMPTY, l, -1, QError.CONSTR,
						Constraint.Kind.LT);
			else
				Constraint
						.addConstraint(deref(argQT), deref(fqt),
								QualGate.EMPTY, l, 0, QError.CONSTR,
								Constraint.Kind.LT);
		}
		ObjectQType nodeQt;
		if (JqualPlugin.contextSensitive) {
			nodeQt = (ObjectQType) mqt.getReturnQType().copy(l,
					mqt.name + "_new@" + l.getLine());

			Constraint.addConstraint(mqt.getReturnQType(), nodeQt,
					QualGate.EMPTY, l, 1, QError.INST, Constraint.Kind.LT);
		} else {
			nodeQt = (ObjectQType) mqt.getReturnQType();
		}

		makeTransparent(nodeQt, l, QError.FFI_NEW_INST_TRANSP);

		nodeQt.setPreferred(false);

		setNodeQType(node, nodeQt);
	}

	public void endVisit(ConditionalExpression node) {
		Location l = getLocation(node);
		QType qt = null;
		QType thenQt = getNodeQType(node.getThenExpression());
		QType elseQt = getNodeQType(node.getElseExpression());

		String name = "conditional";
		ITypeBinding t = node.resolveTypeBinding();
		qt = QType.getFreshQType(t, t.getDimensions(), l, name, false, true);

		Constraint.addConstraint(deref(thenQt), qt, QualGate.EMPTY, l, 0,
				QError.COND, Constraint.Kind.LT);
		Constraint.addConstraint(deref(elseQt), qt, QualGate.EMPTY, l, 0,
				QError.COND, Constraint.Kind.LT);
		setNodeQType(node, qt);
	}

	public void endVisit(ConstructorInvocation node) {
		Location l = getLocation(node);
		IMethodBinding m = node.resolveConstructorBinding();
		setNodeQType(node, sharedConstructorEndVisit(l, m, node.arguments()));
	}

	private QType sharedConstructorEndVisit(Location l, IMethodBinding m,
			List arguments) {
		if (m == null) {
			this.analysis.logMissingClass("constructor at " + l, l);
			return null;
		}
		MethodKey key = MethodKey.getMethodKey(m);
		MethodQType qt = (MethodQType) this.analysis.lookupQType(key);
		if ((qt == null) && (qt == null)) {
			qt = QType.getFreshQType(m, l, false);
			this.analysis.addQType(key, qt);
		}

		Iterator formal = qt.getParameterQTypes().iterator();

		for (Iterator args = arguments.iterator(); args.hasNext();) {
			QType argQT = getNodeQType((Expression) args.next());
			QType fqt = (QType) formal.next();
			if (JqualPlugin.contextSensitive)
				Constraint.addConstraint(deref(argQT), deref(fqt),
						QualGate.EMPTY, l, -1, QError.CONSTR,
						Constraint.Kind.LT);
			else {
				Constraint
						.addConstraint(deref(argQT), deref(fqt),
								QualGate.EMPTY, l, 0, QError.CONSTR,
								Constraint.Kind.LT);
			}

		}

		QType invocationQt = qt.getReturnQType().copy(l,
				key + "_new_instance@" + l.locIndex);

		if (JqualPlugin.contextSensitive) {
			Constraint.addConstraint(qt.getReturnQType(), invocationQt,
					QualGate.EMPTY, l, 1, QError.RETURN, Constraint.Kind.LT);
		} else
			Constraint.addConstraint(qt.getReturnQType(), invocationQt,
					QualGate.EMPTY, l, 0, QError.RETURN, Constraint.Kind.LT);

		QType ret = invocationQt;

		return ret;
	}

	public boolean visit(InfixExpression node) {
		if (node.getOperator().equals(InfixExpression.Operator.PLUS)) {
			ITypeBinding t = node.resolveTypeBinding();
			if ((t != null) && (!t.isPrimitive())) {
				Expression left = node.getLeftOperand();
				MethodInvocation m = wrapper(left);
				node.setLeftOperand(m);
				m.arguments().add(left);

				Expression right = node.getRightOperand();
				m = wrapper(right);
				node.setRightOperand(m);
				m.arguments().add(right);

				if (node.hasExtendedOperands()) {
					ListIterator l = node.extendedOperands().listIterator();
					while (l.hasNext()) {
						Expression o = (Expression) l.next();
						m = wrapper(o);
						l.remove();

						l.add(m);
						m.arguments().add(o);
					}
				}
			}
		}

		return true;
	}

	private MethodInvocation wrapper(Expression operand) {
		AST ast = operand.getAST();
		MethodInvocation methodInvocation = ast.newMethodInvocation();
		methodInvocation.setExpression(ast.newSimpleName("String"));
		methodInvocation.setName(ast.newSimpleName("valueOf"));
		return methodInvocation;
	}

	public void endVisit(InfixExpression node) {
		Location l = getLocation(node);

		if (isBooleanOperator(node.getOperator())) {
			setNodeQType(node, null);

			if ((!(node.getLeftOperand() instanceof NullLiteral))
					&& (!(node.getRightOperand() instanceof NullLiteral))) {
				checkInterned(node.getOperator(),
						getNodeQType(node.getLeftOperand()), l);
				checkInterned(node.getOperator(),
						getNodeQType(node.getRightOperand()), l);
			}
			return;
		}

		Expression left = node.getLeftOperand();
		Expression right = node.getRightOperand();
		QType leftQt = getNodeQType(left);
		QType rightQt = getNodeQType(right);

		if ((leftQt == null) && (rightQt == null)) {
			return;
		}
		if (isAssignmentOperator(node.getOperator().toString())) {
			javariAssignRule(node.getLeftOperand(), l, QError.JAVARI);
		}

		if (leftQt == null) {
			if (!JqualPlugin.inferOpaque) {
				setNodeQType(node, rightQt);
			} else {
				QType nodeQt = rightQt.copy(l,
						"TranparentCopy of " + rightQt.getName());
				Constraint.addConstraint(nodeQt, rightQt, QualGate.EMPTY, l, 0,
						QError.INFIX, Constraint.Kind.LT);
				makeTransparent(nodeQt, l, QError.FFI_OP_TRANSP);
				setNodeQType(node, nodeQt);
			}

			return;
		}

		if (rightQt == null) {
			if (!JqualPlugin.inferOpaque) {
				setNodeQType(node, leftQt);
			} else {
				QType nodeQt = leftQt.copy(l,
						"TranparentCopy of " + leftQt.getName());
				Constraint.addConstraint(nodeQt, leftQt, QualGate.EMPTY, l, 0,
						QError.INFIX, Constraint.Kind.LT);
				makeTransparent(nodeQt, l, QError.FFI_OP_TRANSP);
				setNodeQType(node, nodeQt);
			}

			return;
		}

		leftQt = deref(leftQt);
		rightQt = deref(rightQt);

		QType lub = null;

		if (!leftQt.isPrimitive())
			lub = leftQt.copy(l, "InfixOpTemp");
		else {
			lub = rightQt.copy(l, "InfixOpTemp");
		}
		if (leftQt.isPrimitive())
			CQual.makeEdge(Constraint.Kind.LT, leftQt.getQual(), lub.getQual(),
					QualGate.EMPTY, l, 0, QError.INFIX);
		else {
			Constraint.addConstraint(leftQt, lub, QualGate.EMPTY, l, 0,
					QError.INFIX, Constraint.Kind.LT);
		}
		if (rightQt.isPrimitive())
			CQual.makeEdge(Constraint.Kind.LT, rightQt.getQual(),
					lub.getQual(), QualGate.EMPTY, l, 0, QError.INFIX);
		else {
			Constraint.addConstraint(rightQt, lub, QualGate.EMPTY, l, 0,
					QError.INFIX, Constraint.Kind.LT);
		}
		makeTransparent(lub, l, QError.FFI_OP_TRANSP);
		setNodeQType(node, lub);
	}

	private void checkInterned(InfixExpression.Operator op, QType qt, Location l) {
		if (!JqualPlugin.inferInterned) {
			return;
		}
		if ((op != InfixExpression.Operator.EQUALS)
				&& (op != InfixExpression.Operator.NOT_EQUALS)) {
			return;
		}
		if (qt == null)
			return;
		qt = qt.deref();

		if (!(qt instanceof ObjectQType)) {
			return;
		}

		if ((!l.getFileName().contains("jtdsXid"))
				|| (!this.currentMethodStack.isEmpty())) {
			MethodQType m = (MethodQType) this.currentMethodStack.peek();

			if ((m.shortName.equals("equals"))
					&& (m.getThisQType().getType().equals(qt.getType()))) {
				return;
			}
		}
		Qualifier q = Qualifier.findQualifierConstant("interned");

		CQual.makeEdge(Constraint.Kind.LT, qt.getQual(), q, QualGate.EMPTY, l,
				0, QError.INTERNED);
	}

	public boolean visit(MethodDeclaration node) {
		Location l = getLocation(node);
		MethodKey key = null;
		try {
			key = MethodKey.getMethodKey(node.resolveBinding());
		} catch (NullPointerException localNullPointerException) {
			throw new NullPointerException("null method binding at " + l
					+ " for " + node);
		}
		MethodQType qt = (MethodQType) this.analysis.lookupQType(key);
		if (qt == null) {
			throw new RuntimeException(
					"Did not make QType during first pass for " + key);
		}

		this.currentMethodStack.push(qt);
		this.fieldContext.push(qt.getThisQType());
		return true;
	}

	public void endVisit(MethodDeclaration node) {
		this.currentMethodStack.pop();
		this.fieldContext.pop();
	}

	public boolean visit(TypeDeclaration node) {
		ITypeBinding t = node.resolveBinding();
		String key = t.getKey();
		ClassQType cqt = this.analysis.getClassQType(t);
		if (cqt == null) {
			throw new RuntimeException(
					"Did not make QType during first pass for " + key);
		}
		this.fieldContext.push(cqt);
		return true;
	}

	public void endVisit(TypeDeclaration node) {
		this.fieldContext.pop();
	}

	public void endVisit(MethodInvocation node) {
		sharedMethodInvocationEndVisit(node);
	}

	private void sharedMethodInvocationEndVisit(Expression node) {
		Location l = getLocation(node);

		Expression target = null;
		IMethodBinding m = null;
		List<Expression> args = null;
		String name = null;
		if ((node instanceof MethodInvocation)) {
			MethodInvocation mnode = (MethodInvocation) node;
			name = mnode.getName().toString();
			m = mnode.resolveMethodBinding();

			if (m == null) {
				if ((name.equals("String")) || (name.equals("valueOf"))) {
					return;
				}
				this.analysis.logMissingClass(name, l);
				return;
			}

			target = mnode.getExpression();
			args = mnode.arguments();
		} else if ((node instanceof SuperMethodInvocation)) {
			SuperMethodInvocation snode = (SuperMethodInvocation) node;
			name = snode.getName().toString();
			m = snode.resolveMethodBinding();

			if (m == null) {
				this.analysis.logMissingClass(snode.getName().toString(), l);
				return;
			}
			target = null;
			args = snode.arguments();
		} else {
			throw new IllegalArgumentException(
					"node should always be a MethodInvocation or a SuperMethodInvocation. "
							+ node.getClass());
		}

		MethodKey key = MethodKey.getMethodKey(m);
		this.analysis.methodsUsed.add(key);
		MethodQType qt = (MethodQType) this.analysis.lookupQType(key);
		if (qt == null) {
			qt = QType.getFreshQType(m, l, false);
			this.analysis.addQType(key, qt);
		}

		if (qt.getReturnQType() != null) {
			if (JqualPlugin.contextSensitive) {
				QType invocationQt = qt.getReturnQType().copy(l,
						qt.getName() + "_invRet@" + l.locIndex);
				invocationQt.setPreferred(false);

				Constraint
						.addConstraint(qt.getReturnQType(), invocationQt,
								QualGate.EMPTY, l, 1, QError.RETURN,
								Constraint.Kind.LT);
				setNodeQType(node, invocationQt);
			} else {
				setNodeQType(node, qt.getReturnQType());
			}

		}

		QType targetQt = null;
		if (target == null) {
			targetQt = (QType) this.fieldContext.peek();
		} else
			targetQt = getNodeQType(target);
		QualGate gate;
		if ((JqualPlugin.inferReadonly) && (inConstructor()))
			gate = this.analysis.gateBlockJavari;
		else {
			gate = QualGate.EMPTY;
		}

		if (!qt.isStatic) {
			if (JqualPlugin.contextSensitive)
				Constraint.addConstraint(deref(targetQt), qt.getThisQType(),
						gate, l, -1, QError.INVOKE, Constraint.Kind.LT);
			else {
				Constraint.addConstraint(deref(targetQt), qt.getThisQType(),
						gate, l, 0, QError.INVOKE, Constraint.Kind.LT);
			}

		}

		Iterator f = qt.getParameterQTypes().iterator();
		for (Expression arg : args) {
			QType aqt = getNodeQType(arg);
			Object next;
			try {
				next = f.next();
			} catch (NoSuchElementException localNoSuchElementException) {

				throw new IllegalArgumentException("Call to " + name + " at "
						+ getLocation(node) + " expects " + args.size()
						+ " formal arguments, received "
						+ qt.getParameterQTypes().size());
			}
			if (JqualPlugin.contextSensitive)
				Constraint.addConstraint(deref(aqt), deref((QType) next),
						QualGate.EMPTY, l, -1, QError.INVOKE,
						Constraint.Kind.LT);
			else
				Constraint
						.addConstraint(deref(aqt), deref((QType) next),
								QualGate.EMPTY, l, 0, QError.INVOKE,
								Constraint.Kind.LT);
		}
	}

	public void endVisit(ParenthesizedExpression node) {
		setNodeQType(node, getNodeQType(node.getExpression()));
	}

	public void endVisit(PostfixExpression node) {
		Location l = getLocation(node);
		QType qt = getNodeQType(node.getOperand());
		javariAssignRule(node.getOperand(), l, QError.JAVARI);

		qt = deref(qt);
		makeTransparent(qt, l, QError.FFI_OP_TRANSP);
		setNodeQType(node, qt);
	}

	public void endVisit(PrefixExpression node) {
		Location l = getLocation(node);

		QType qt = getNodeQType(node.getOperand());
		qt = deref(qt);

		if ((node.getOperator().equals(PrefixExpression.Operator.DECREMENT))
				|| (node.getOperator()
						.equals(PrefixExpression.Operator.INCREMENT))) {
			javariAssignRule(node.getOperand(), l, QError.JAVARI);
			makeTransparent(qt, l, QError.FFI_OP_TRANSP);
		} else {
			makeTransparent(qt, l, QError.FFI_OP_TRANSP);
		}

		setNodeQType(node, qt);
	}

	public boolean visit(QualifiedName node) {
		IBinding b = node.resolveBinding();
		if ((b instanceof ITypeBinding)) {
			setNodeQType(node, this.analysis.getClassQType((ITypeBinding) b));
		} else if ((b instanceof IVariableBinding)) {
			ASTNode qnode = node.getQualifier();
			ASTNode nnode = node.getName();
			setNodeQType(
					node,
					sharedFieldAccessVisit(getLocation(node), node, qnode,
							nnode));
		}

		return false;
	}

	private boolean isLength(SimpleName name) {
		IBinding nb = name.resolveBinding();
		String nameKey = nb.getKey();

		return nameKey.equals(".length)I");
	}

	public void endVisit(ReturnStatement node) {
		Location l = getLocation(node);

		if (node.getExpression() != null) {
			QType qt = getNodeQType(node.getExpression());
			QType rqt = getCurrentMethod().getReturnQType();
			Constraint.addConstraint(deref(qt), rqt, QualGate.EMPTY, l, 0,
					QError.RETURN, Constraint.Kind.LT);
		}
	}

	public boolean visit(BreakStatement node) {
		return false;
	}

	public boolean visit(ContinueStatement node) {
		return false;
	}

	public void endVisit(SimpleName node) {
		if ((node.getParent() instanceof LabeledStatement)) {
			return;
		}
		Location l = getLocation(node);
		IBinding b = node.resolveBinding();

		if (b == null) {
			if ((node.getFullyQualifiedName().equals("String"))
					|| (node.getFullyQualifiedName().equals("valueOf"))) {
				return;
			}
			this.analysis.logMissingClass(node.getFullyQualifiedName(), l);
		} else if ((b instanceof IVariableBinding)) {
			IVariableBinding v = (IVariableBinding) b;
			RefQType nodeQt = null;
			String key = v.getKey();
			if (isLength(node)) {
				nodeQt = null;
			} else if (v.isField()) {
				ObjectQType enclosingQt = (ObjectQType) this.fieldContext
						.peek();
				RefQType fieldQt = enclosingQt.lookupFieldQType(v);
				if (fieldQt == null) {
					String name = v.getName();
					fieldQt = QType.getFreshRefQType(v.getType(), v.getType()
							.getDimensions(), l, name, false, JqualPlugin
							.isStatic(v));

					if ((JqualPlugin.inferOpaque)
							&& (JqualPlugin.ffiInferPublic)
							&& ((v.getModifiers() & 0x1) != 0)) {
						CQual.makeEdge(Constraint.Kind.LT,
								JqualPlugin.analysis.qual_public, fieldQt
										.deref().getQual(), QualGate.EMPTY, l,
								0, QError.FFI_PUBLIC_FIELD);
					}
					try {
						enclosingQt.addFieldQType(v, fieldQt);
					} catch (NullPointerException localNullPointerException) {
						throw new NullPointerException("At " + l + " " + node
								+ " Field Name: " + v.getName());
					}
				}
				setNodeObjectContext(node, enclosingQt);
				createJavariFieldAccessConstraints(enclosingQt, fieldQt, l);
				nodeQt = fieldQt;
			} else {
				nodeQt = (RefQType) this.analysis.lookupQType(key);
				if (nodeQt == null) {
					String name = this.analysis.createName(v);
					ITypeBinding t = v.getType();
					if (t != null) {
						nodeQt = QType.getFreshRefQType(v.getType(), v
								.getType().getDimensions(), l, name, false,
								false);
						this.analysis.addQType(key, nodeQt);
					} else {
						this.analysis.logMissingClass(v.getName(), l);
					}
				}
			}
			setNodeQType(node, nodeQt);
		} else if ((b instanceof ITypeBinding)) {
			ITypeBinding t = (ITypeBinding) b;
			TypeKey key = TypeKey.getTypeKey(t);
			ClassQType nodeQt = this.analysis.lookupClassQType(key);
			if (nodeQt == null) {
				String name = this.analysis.createName(t);
				Profile.logMemStart("endVisit(SimpleName)/new ClassQType");
				nodeQt = new ClassQType(t, l, name.toUpperCase(), false);
				Profile.logMemEnd();
				this.analysis.addClassQType(key, nodeQt);
			}

			setNodeQType(node, nodeQt);
		}
	}

	private void createJavariFieldAccessConstraints(ObjectQType qualifierQt,
			RefQType nodeQt, Location l) {
		if ((JqualPlugin.inferReadonly) && (qualifierQt != null)) {
			if (nodeQt.isStatic) {
				return;
			}

			if (this.analysis.mutable(nodeQt.getKey())) {
				return;
			}

			if (inConstructor()) {
				return;
			}

			Qualifier q1 = qualifierQt.getQual();
			Qualifier q3 = deref(nodeQt).getQual();

			CQual.makeEdge(Constraint.Kind.LT, q1, q3, QualGate.EMPTY, l, 0,
					QError.FIELD);
		}
	}

	// private boolean isLocal(IVariableBinding v) {
	// return !v.isField();
	// }

	public void endVisit(SingleVariableDeclaration node) {
		Location l = getLocation(node);
		IVariableBinding v = node.resolveBinding();
		String key = v.getKey();

		QType qt = this.analysis.lookupQType(key);
		if (qt == null) {
			throw new RuntimeException(
					"Found SingleVariableDeclarationNode without a qtype: "
							+ key);
		}

		if (node.getInitializer() != null) {
			QType iqt = getNodeQType(node.getInitializer());
			Constraint.addConstraint(iqt, qt, QualGate.EMPTY, l, 0,
					QError.INIT, Constraint.Kind.LT);
		}
	}

	public void endVisit(SuperConstructorInvocation node) {
		Location l = getLocation(node);
		IMethodBinding m = node.resolveConstructorBinding();
		setNodeQType(node, sharedConstructorEndVisit(l, m, node.arguments()));
	}

	public boolean visit(SuperFieldAccess node) {
		Location l = getLocation(node);
		IVariableBinding v = node.resolveFieldBinding();
		FieldKey key = FieldKey.getFieldKey(v);

		RefQType qt = (RefQType) this.analysis.lookupQType(key);

		if (qt == null) {
			ClassQType cqt = this.analysis.lookupClassQType(TypeKey
					.getTypeKey(v.getDeclaringClass()));
			int d = v.getType().getDimensions();
			qt = QType.getFreshRefQType(v.getType(), d, l, cqt.getName() + "."
					+ v.getName(), this.original, JqualPlugin.isStatic(v));
			cqt.addFieldQType(v, qt);
		}

		setNodeQType(node, qt);
		return true;
	}

	public void endVisit(SuperMethodInvocation node) {
		sharedMethodInvocationEndVisit(node);
	}

	public boolean visit(ThisExpression node) {
		getLocation(node);
		QType qt = (QType) this.fieldContext.peek();
		setNodeQType(node, qt);
		return true;
	}

	public boolean visit(VariableDeclarationFragment node) {
		if (node.getInitializer() != null) {
			node.getInitializer().accept(this);
		}
		return false;
	}

	public void endVisit(VariableDeclarationFragment node) {
		Location l = getLocation(node);
		IVariableBinding v = node.resolveBinding();
		if (v == null) {
			return;
		}
		if (node.getInitializer() != null) {
			QType iqt = getNodeQType(node.getInitializer());

			if (iqt != null) {
				RefQType qt = null;
				if (v.isField()) {
					ITypeBinding t = v.getDeclaringClass();
					ClassQType cqt = this.analysis.getClassQType(t);
					qt = cqt.lookupFieldQType(v);

					if (qt == null) {
						int d = v.getType().getDimensions();
						qt = QType.getFreshRefQType(v.getType(), d, l,
								cqt.getName() + "." + v.getName(),
								this.original, JqualPlugin.isStatic(v));
						cqt.addFieldQType(v, qt);

						if ((JqualPlugin.inferOpaque)
								&& (JqualPlugin.ffiInferPublic)
								&& ((v.getModifiers() & 0x1) != 0))
							CQual.makeEdge(Constraint.Kind.LT,
									JqualPlugin.analysis.qual_public, qt
											.deref().getQual(), QualGate.EMPTY,
									l, 0, QError.FFI_PUBLIC_FIELD);
					}
				} else {
					qt = (RefQType) this.analysis.lookupQType(v.getKey());
					if (qt == null)
						throw new IllegalArgumentException(
								"Could not find QType for " + v.getKey());
				}
				Constraint.addConstraint(deref(iqt), deref(qt), QualGate.EMPTY,
						l, 0, QError.INIT, Constraint.Kind.LT);
			}
		}
	}

	public boolean visit(FieldAccess node) {
		Location l = getLocation(node);
		ASTNode qnode = node.getExpression();
		ASTNode nnode = node.getName();
		setNodeQType(node, sharedFieldAccessVisit(l, node, qnode, nnode));

		return false;
	}

	private RefQType sharedFieldAccessVisit(Location l, ASTNode node,
			ASTNode qnode, ASTNode nnode) {
		qnode.accept(this);

		ObjectQType qualifierQt = (ObjectQType) getNodeQType(qnode).deref();

		if (qualifierQt == null) {
			JqualPlugin.printlnLog("missingClassLog", qnode + "(" + l + ")");
			throw new NullPointerException("Null QTYPE: " + qnode + "(" + l
					+ ")");
		}

		this.fieldContext.push(qualifierQt);

		nnode.accept(this);
		RefQType nameQt = (RefQType) getNodeQType(nnode);
		setNodeObjectContext(node, qualifierQt);

		this.fieldContext.pop();

		return nameQt;
	}

	private QType getNodeQType(ASTNode node) {
		return (QType) node.getProperty("QType");
	}

	private void setNodeQType(ASTNode node, QType type) {
		if ((type != null)
				&& (type.getName() != null)
				&& (type.getName()
						.equals("Ljava/lang/Float;.floatValue()F_this"))) {
			JqualPlugin.out.println(type.getClass());
			JqualPlugin.out.println("At " + node);
			Thread.dumpStack();
		}
		node.setProperty("QType", type);
	}

	private void setNodeObjectContext(ASTNode node, ObjectQType cqt) {
		node.setProperty("object_context", cqt);
	}

	private ObjectQType getNodeObjectContext(ASTNode node) {
		return (ObjectQType) node.getProperty("object_context");
	}

	public static boolean isBooleanOperator(InfixExpression.Operator o) {
		return (o.equals(InfixExpression.Operator.CONDITIONAL_AND))
				|| (o.equals(InfixExpression.Operator.CONDITIONAL_OR))
				|| (o.equals(InfixExpression.Operator.EQUALS))
				|| (o.equals(InfixExpression.Operator.GREATER))
				|| (o.equals(InfixExpression.Operator.LESS))
				|| (o.equals(InfixExpression.Operator.GREATER_EQUALS))
				|| (o.equals(InfixExpression.Operator.LESS_EQUALS))
				|| (o.equals(InfixExpression.Operator.NOT_EQUALS));
	}

	public static boolean isAssignmentOperator(String op) {
		return Assignment.Operator.toOperator(op) != null;
	}

	private MethodQType getCurrentMethod() {
		if (this.currentMethodStack.isEmpty()) {
			return null;
		}
		QType qt = (QType) this.currentMethodStack.peek();
		if (!(qt instanceof MethodQType))
			throw new IllegalArgumentException(
					"Called getCurrentMethod from within initializer.");
		return (MethodQType) qt;
	}

	// private QType getCurrentMethodContext() {
	// return (QType) this.currentMethodStack.peek();
	// }

	private Location getLocation(ASTNode node) {
		return Location.getLocation(node, this.compilationUnit, this.fileName);
	}

	private QType deref(QType qt) {
		if (qt != null) {
			return qt.deref();
		}
		return null;
	}

	public static boolean isStatic(MethodDeclaration m) {
		return (m.getModifiers() & 0x8) != 0;
	}
}